package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.Images;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.common.Sleeper;
import it.unibo.refolding.pico.ApproachRetractionAssistant.TotalTimeVelIndex;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointAndSlope;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointsAndSlope;
import it.unibo.refolding.pico.PicoDataAnalyser.PeakAnalysisInput;
import it.unibo.refolding.pico.stripchart.AnalysisOptions;
import it.unibo.refolding.pico.stripchart.JpHelper;
import it.unimi.dsi.fastutil.doubles.DoubleList;

import java.awt.Color;
import java.awt.Shape;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.Parser;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.commons.math3.stat.descriptive.rank.Percentile;
import org.codehaus.plexus.util.ExceptionUtils;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

public class Refolding {

  private static final Logger logger = Logger.getLogger(Refolding.class.getSimpleName());

  private static boolean ENABLE_SVG = true;

  public static void main(String[] args) throws ParseException, InterruptedException {
    System.setProperty("java.awt.headless", "true");

    Options options = new Options();

    Option nanoscopeVersionOption = new Option("nanoscopeVersion", true, "3 or 5");
    nanoscopeVersionOption.setRequired(true);
    options.addOption(nanoscopeVersionOption);
    
    Option allowedTimeRelativeErrorOption = AnalysisOptions.createAllowedTimeRelativeErrorOption();
    options.addOption(allowedTimeRelativeErrorOption);

    Option refoldAllowedTimeRelativeErrorOption = AnalysisOptions.createRefoldAllowedTimeRelativeErrorOption();
    options.addOption(refoldAllowedTimeRelativeErrorOption);

    CommonRefoldingOptions commonRefoldingOptions = CommonRefoldingOptions.create();
    commonRefoldingOptions.addTo(options);
    CommonClRefoldingOptions commonClRefoldingOptions = CommonClRefoldingOptions.create();
    commonClRefoldingOptions.addTo(options);

    Option defLsbVsZLsbSlopeOption = PicoUtil.createDefLsbVsZLsbSlopeOption();
    options.addOption(defLsbVsZLsbSlopeOption);

    Option svgDefScaleOption = PicoUtil.createSvgDefScaleOption();
    options.addOption(svgDefScaleOption);
    
    Option svgIterationOffsetPnOption = PicoUtil.createSvgIterationOffsetPnOption();
    options.addOption(svgIterationOffsetPnOption);

    Option driverTypeOption = NanoDriverUtil.createDriverTypeOption();
    options.addOption(driverTypeOption);

    Option junctionPointsDirOption = JpHelper.createJunctionPointsOption();
    options.addOption(junctionPointsDirOption);

    Option contactPointAndSlopeDirOption = ContactPointAndSlopeHelper.createContactPointAndSlopeDirOption();
    options.addOption(contactPointAndSlopeDirOption);

    Option peakSlopesDirOption = PeakSlopesHelper.createPeakSlopesDirOption();
    options.addOption(peakSlopesDirOption);

    Option endOfCurveRangeNmOption = new Option("endOfCurveRangeNm", true, "");
    endOfCurveRangeNmOption.setRequired(true);
    endOfCurveRangeNmOption.setType(Double.TYPE);
    options.addOption(endOfCurveRangeNmOption);

    Option fetchMoleculeRampSizeNmOption = new Option("fetchMoleculeRampSizeNm", true, "");
    fetchMoleculeRampSizeNmOption.setRequired(true);
    fetchMoleculeRampSizeNmOption.setType(Double.TYPE);
    options.addOption(fetchMoleculeRampSizeNmOption);

    Option breakAwayRampSizeNmOption = new Option("breakAwayRampSizeNm", true, "");
    breakAwayRampSizeNmOption.setRequired(true);
    breakAwayRampSizeNmOption.setType(Double.TYPE);
    options.addOption(breakAwayRampSizeNmOption);

    Option loseRampSizeNmOption = new Option("loseRampSizeNm", true, "");
    loseRampSizeNmOption.setRequired(true);
    loseRampSizeNmOption.setType(Double.TYPE);
    options.addOption(loseRampSizeNmOption);

    Option refoldApproachRampSizeNmOption = new Option("refoldApproachRampSizeNm", true, "");
    refoldApproachRampSizeNmOption.setRequired(true);
    refoldApproachRampSizeNmOption.setType(Double.TYPE);
    options.addOption(refoldApproachRampSizeNmOption);

    Option loseVelocityNmPerSOption = new Option(
      "loseVelocityNmPerS", true,
      "Nanoscope V has fixed StripChartRate=500 Hz, so we recommend 198 nm/s");
    loseVelocityNmPerSOption.setRequired(true);
    loseVelocityNmPerSOption.setType(Double.TYPE);
    options.addOption(loseVelocityNmPerSOption);

    Option parkingVelocityNmPerSOption = new Option(
      "parkingVelocityNmPerS", true, "");
    parkingVelocityNmPerSOption.setRequired(true);
    parkingVelocityNmPerSOption.setType(Double.TYPE);
    options.addOption(parkingVelocityNmPerSOption);

    Option pushNmOption = new Option("pushNm", true, "");
    pushNmOption.setRequired(true);
    pushNmOption.setType(Double.TYPE);
    options.addOption(pushNmOption);

    Option maxRectificationNmOption = new Option("maxRectificationNm", true, "");
    maxRectificationNmOption.setRequired(true);
    maxRectificationNmOption.setType(Double.TYPE);
    options.addOption(maxRectificationNmOption);

    Option hoverNmOption = new Option("hoverNm", true, "");
    hoverNmOption.setRequired(true);
    hoverNmOption.setType(Double.TYPE);
    options.addOption(hoverNmOption);

    Option parkingHoverNmOption = new Option("parkingHoverNm", true, "");
    parkingHoverNmOption.setRequired(true);
    parkingHoverNmOption.setType(Double.TYPE);
    options.addOption(parkingHoverNmOption);

    Option refoldApproachVelNmPerSOption = new Option("refoldApproachVelNmPerS", true, "");
    refoldApproachVelNmPerSOption.setRequired(true);
    refoldApproachVelNmPerSOption.setType(Double.TYPE);
    options.addOption(refoldApproachVelNmPerSOption);

    Option minimumPeakForcePnOption = new Option("minimumPeakForcePn", true, "");
    minimumPeakForcePnOption.setRequired(true);
    minimumPeakForcePnOption.setType(Double.TYPE);
    options.addOption(minimumPeakForcePnOption);
    
    Option refoldCountOption = new Option("refoldCount", true, "");
    refoldCountOption.setRequired(true);
    options.addOption(refoldCountOption);

    Option minPeakForcePnOption = AnalysisOptions.createMinPeakForcePnOption();
    options.addOption(minPeakForcePnOption);

    Option minPeakContourLengthNmOption = AnalysisOptions.createMinPeakContourLengthNmOption();
    options.addOption(minPeakContourLengthNmOption);

    Option minPeakForceRelativeToEmpiricalBaselinePnOption = AnalysisOptions.createMinPeakForceRelativeToEmpiricalBaselinePnOption();
    options.addOption(minPeakForceRelativeToEmpiricalBaselinePnOption);

    Option medianAbsoluteDeviationLowerBoundPnOption = AnalysisOptions.createMedianAbsoluteDeviationLowerBoundPnOption();
    options.addOption(medianAbsoluteDeviationLowerBoundPnOption);

    Option leftMostlyHigherWindowSizeNmOption = AnalysisOptions.createLeftMostlyHigherWindowSizeNmOption();
    options.addOption(leftMostlyHigherWindowSizeNmOption);

    Option leftMostlyHigherFractionOption = AnalysisOptions.createLeftMostlyHigherFractionOption();
    options.addOption(leftMostlyHigherFractionOption);

    Option leftMostlyHigherPnOption = AnalysisOptions.createLeftMostlyHigherPnOption();
    options.addOption(leftMostlyHigherPnOption);

    Option rightMuchHigherThanAllPnOption = AnalysisOptions.createRightMuchHigherThanAllPnOption();
    options.addOption(rightMuchHigherThanAllPnOption);

    Option rightMuchHigherPnOption = AnalysisOptions.createRightMuchHigherPnOption();
    options.addOption(rightMuchHigherPnOption);

    Option atMostOnePeakWindowNmOption = AnalysisOptions.createAtMostOnePeakWindowNmOption();
    options.addOption(atMostOnePeakWindowNmOption);

    Option extendedForceRangeStartPnOption = new Option("extendedForceRangeStartPn", true, "");
    extendedForceRangeStartPnOption.setRequired(true);
    extendedForceRangeStartPnOption.setType(Double.TYPE);
    options.addOption(extendedForceRangeStartPnOption);

    Option extendedForceRangeEndPnOption = new Option("extendedForceRangeEndPn", true, "");
    extendedForceRangeEndPnOption.setRequired(true);
    extendedForceRangeEndPnOption.setType(Double.TYPE);
    options.addOption(extendedForceRangeEndPnOption);

    Option lastResortMinimumForcePnOption = new Option("lastResortMinimumForcePn", true, "");
    lastResortMinimumForcePnOption.setRequired(true);
    lastResortMinimumForcePnOption.setType(Double.TYPE);
    options.addOption(lastResortMinimumForcePnOption);

//    Option baselineFractionOption = new Option(
//      "baselineFraction",
//      true,
//      "Fraction for empirical contact point identification. Negative means use previous (breakAway) contact point.");
//    baselineFractionOption.setRequired(true);
//    baselineFractionOption.setType(Double.TYPE);
//    options.addOption(baselineFractionOption);

    Option forceRangePiezoStepNmOption = new Option(
      "forceRangePiezoStepNm", true, "If non-zero, it overrides wlc.");
    forceRangePiezoStepNmOption.setRequired(true);
    forceRangePiezoStepNmOption.setType(Double.TYPE);
    options.addOption(forceRangePiezoStepNmOption);

    Option preserveRampSizePiezoStepNmOption = new Option(
      "preserveRampSizePiezoStepNm", true, "Step size once desired force is reached.");
    preserveRampSizePiezoStepNmOption.setRequired(true);
    preserveRampSizePiezoStepNmOption.setType(Double.TYPE);
    options.addOption(preserveRampSizePiezoStepNmOption);

    Option numberOfSamplesOption = new Option("numberOfSamples", true, "");
    numberOfSamplesOption.setRequired(true);
    numberOfSamplesOption.setType(Integer.TYPE);
    options.addOption(numberOfSamplesOption);

    Option linkerPerModuleNmOption = new Option("linkerPerModuleNm", true, "");
    linkerPerModuleNmOption.setRequired(true);
    linkerPerModuleNmOption.setType(Double.TYPE);
    options.addOption(linkerPerModuleNmOption);

    Option allowSkippedFetchOption = new Option(
      "allowSkippedFetch", true, "how many skipped peaks to allow for");
    allowSkippedFetchOption.setRequired(true);
    allowSkippedFetchOption.setType(Long.TYPE);
    options.addOption(allowSkippedFetchOption);

    Option allowSkippedRefoldOption = new Option(
      "allowSkippedRefold", true, "how many skipped peaks to allow for");
    allowSkippedRefoldOption.setRequired(true);
    allowSkippedRefoldOption.setType(Long.TYPE);
    options.addOption(allowSkippedRefoldOption);

    Option syncRefoldApproachRampSizeOption = new Option(
      "syncRefoldApproachRampSize", true, "Change approach ramp size as retraction ramp size is changed.");
    syncRefoldApproachRampSizeOption.setRequired(true);
    syncRefoldApproachRampSizeOption.setType(Boolean.TYPE);
    options.addOption(syncRefoldApproachRampSizeOption);

    Option preserveRampSizeOption = new Option(
      "preserveRampSize", true, "Change ZScanStart but leave RampSize fixed.");
    preserveRampSizeOption.setRequired(true);
    preserveRampSizeOption.setType(Boolean.TYPE);
    options.addOption(preserveRampSizeOption);

    Option maxRequiredWellSpacedAtEndOption = new Option(
      "maxRequiredWellSpacedAtEnd", true, "max number of well spaced peaks to require at end");
    maxRequiredWellSpacedAtEndOption.setRequired(true);
    maxRequiredWellSpacedAtEndOption.setType(Long.TYPE);
    options.addOption(maxRequiredWellSpacedAtEndOption);

    Option fetchMoleculeSurfaceDelayMillisOption = new Option(
      "fetchMoleculeSurfaceDelayMillis", true, "how long to hover above the surface");
    fetchMoleculeSurfaceDelayMillisOption.setRequired(true);
    fetchMoleculeSurfaceDelayMillisOption.setType(Long.TYPE);
    options.addOption(fetchMoleculeSurfaceDelayMillisOption);

    Option hoverForFullRefoldingMillisOption = new Option(
      "hoverForFullRefoldingMillis", true, "how long to hover above the surface");
    hoverForFullRefoldingMillisOption.setRequired(true);
    hoverForFullRefoldingMillisOption.setType(Long.TYPE);
    options.addOption(hoverForFullRefoldingMillisOption);

    Option hoverForSomeRefoldingMillisOption = new Option(
      "hoverForSomeRefoldingMillis", true, "how long to hover above the surface");
    hoverForSomeRefoldingMillisOption.setRequired(true);
    options.addOption(hoverForSomeRefoldingMillisOption);

    Option moleculeTimeLimitMillisOption = new Option(
      "moleculeTimeLimitMillis", true, "how long to keep one molecule");
    moleculeTimeLimitMillisOption.setRequired(true);
    options.addOption(moleculeTimeLimitMillisOption);

    Option maxIterationsOption = new Option("maxIterations", true, "For functional tests.");
    maxIterationsOption.setRequired(true);
    maxIterationsOption.setType(Integer.TYPE);
    options.addOption(maxIterationsOption);

    Option backgroundThreadsOption = new Option("backgroundThreads", true, "For functional tests.");
    backgroundThreadsOption.setRequired(true);
    backgroundThreadsOption.setType(Integer.TYPE);
    options.addOption(backgroundThreadsOption);

    Option asyncJpCpsPsiOption = new Option("asyncJpCpsPsi", true, "For functional tests.");
    asyncJpCpsPsiOption.setRequired(true);
    asyncJpCpsPsiOption.setType(Boolean.TYPE);
    options.addOption(asyncJpCpsPsiOption);

    Option svgDirOption = AnalysisOptions.createSvgDirOption();
    options.addOption(svgDirOption);

    // Mostly useful when the slope is not fixed.
    Option approachSkipDistanceNmOption = PicoUtil.createApproachSkipDistanceNmOption();
    options.addOption(approachSkipDistanceNmOption);

    Option temperatureKOption = AnalysisOptions.createTemperatureKOption();
    options.addOption(temperatureKOption);

    Option persistenceLengthNmOption = AnalysisOptions.createPersistenceLengthNmOption();
    options.addOption(persistenceLengthNmOption);

    Option xOffsetStdDevNmOption = PicoUtil.createXOffsetStdDevNmOption();
    options.addOption(xOffsetStdDevNmOption);

    Option xOffsetChangeFrequencyOption = PicoUtil.createXOffsetChangeFrequencyOption();
    options.addOption(xOffsetChangeFrequencyOption);

    Option equalTimeOption = ApproachRetractionAssistant.createEqualTimeOption();
    options.addOption(equalTimeOption);

    Option reverseVelocitiesNmPerSOption = ApproachRetractionAssistant.createReverseVelocitiesNmPerSOption();
    options.addOption(reverseVelocitiesNmPerSOption);

    Option updateRefoldMillisOption = new Option(
      "updateRefoldMillis", true, "For functional tests.");
    updateRefoldMillisOption.setRequired(true);
    updateRefoldMillisOption.setType(Double.TYPE);
    options.addOption(updateRefoldMillisOption);

    Parser parser = new GnuParser();
    CommandLine line = parser.parse(options, args);
    String nanoscopeVersion = line.getOptionValue(nanoscopeVersionOption.getOpt());
    DoubleList allowedTimeRelativeError = AnalysisOptions.allowedTimeRelativeError(line, allowedTimeRelativeErrorOption);
    DoubleList refoldAllowedTimeRelativeError = AnalysisOptions.refoldAllowedTimeRelativeError(line, refoldAllowedTimeRelativeErrorOption);
    double svgIterationOffsetN = Double.parseDouble(line.getOptionValue(svgIterationOffsetPnOption.getOpt())) / 1e12;
    double svgDefScale = Double.parseDouble(line.getOptionValue(svgDefScaleOption.getOpt()));
    long nanoDriverDelayMillis = commonRefoldingOptions.getNanoDriverDelayMillis(line);
    double approachSkipDistanceM = Double.parseDouble(line.getOptionValue(approachSkipDistanceNmOption.getOpt())) / 1e9;
    String defLsbVsZLsbSlopeString = line.getOptionValue(defLsbVsZLsbSlopeOption.getOpt());
    int loopCount = Integer.parseInt(line.getOptionValue(refoldCountOption.getOpt()));
    double slope = StringUtils.isEmpty(defLsbVsZLsbSlopeString) ? Double.NaN
      : Double.parseDouble(defLsbVsZLsbSlopeString);
    Path captureDir = commonRefoldingOptions.getCaptureDirectory(line);
    Path targetDir = commonRefoldingOptions.getTargetDirectory(line);
    long sleepMillis = commonRefoldingOptions.getSleepMillis(line);
    long waitWhileEmptyMillis = commonRefoldingOptions.getWaitWhileEmptyMillis(line);
    String nanoDriverClassname = commonRefoldingOptions.getNanoDriverClassname(line);
    String nanoDriverHost = commonRefoldingOptions.getNanoDriverHost(line);
    int nanoDriverPort = commonRefoldingOptions.getNanoDriverPort(line);
    double fetchMoleculeRampSizeNm = Double.parseDouble(line.getOptionValue(fetchMoleculeRampSizeNmOption.getOpt()));
    double breakAwayRampSizeNm = Double.parseDouble(line.getOptionValue(breakAwayRampSizeNmOption.getOpt()));
    double loseRampSizeNm = Double.parseDouble(line.getOptionValue(loseRampSizeNmOption.getOpt()));
    double refoldApproachRampSizeNm = Double.parseDouble(line.getOptionValue(refoldApproachRampSizeNmOption.getOpt()));
    double loseVelocityNmPerS = Double.parseDouble(line.getOptionValue(loseVelocityNmPerSOption.getOpt()));
    double parkingVelocityNmPerS = Double.parseDouble(line.getOptionValue(parkingVelocityNmPerSOption.getOpt()));
    double pushNm = Double.parseDouble(line.getOptionValue(pushNmOption.getOpt()));
    double linkerPerModuleNm = Double.parseDouble(line.getOptionValue(linkerPerModuleNmOption.getOpt()));
    double maxRectificationNm = Double.parseDouble(line.getOptionValue(maxRectificationNmOption.getOpt()));
    double hoverNm = Double.parseDouble(line.getOptionValue(hoverNmOption.getOpt()));
    double parkingHoverNm = Double.parseDouble(line.getOptionValue(parkingHoverNmOption.getOpt()));
    double refoldApproachVelNmPerS = Double.parseDouble(line.getOptionValue(refoldApproachVelNmPerSOption.getOpt()));
    double fetchMoleculeSurfaceDelayMillis = Double.parseDouble(line.getOptionValue(fetchMoleculeSurfaceDelayMillisOption.getOpt()));
    double minPeakForceRelativeToEmpiricalBaselineN = AnalysisOptions.minPeakForceRelativeToEmpiricalBaselineN(
      line, minPeakForceRelativeToEmpiricalBaselinePnOption);
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN = AnalysisOptions.medianAbsoluteDeviationLowerBoundN(
      line, medianAbsoluteDeviationLowerBoundPnOption);
    double leftMostlyHigherWindowSizeM = AnalysisOptions.leftMostlyHigherWindowSizeM(
      line, leftMostlyHigherWindowSizeNmOption);
    double leftMostlyHigherFraction = AnalysisOptions.leftMostlyHigherFraction(
      line, leftMostlyHigherFractionOption);
    double leftMostlyHigherN = AnalysisOptions.leftMostlyHigherN(
      line, leftMostlyHigherPnOption);
    Function<? super Double, Double> rightMuchHigherThanAllN = AnalysisOptions.rightMuchHigherThanAllN(
      line, rightMuchHigherThanAllPnOption);
    Function<? super Double, Double> rightMuchHigherN = AnalysisOptions.rightMuchHigherN(
      line, rightMuchHigherPnOption);
    double atMostOnePeakWindowM = AnalysisOptions.atMostOnePeakWindowM(
      line, atMostOnePeakWindowNmOption);
    double extendedForceRangeStartPn = Double.parseDouble(line.getOptionValue(extendedForceRangeStartPnOption.getOpt()));
    double extendedForceRangeEndPn = Double.parseDouble(line.getOptionValue(extendedForceRangeEndPnOption.getOpt()));
    double temperatureK = AnalysisOptions.temperatureK(line, temperatureKOption);
    double persistenceLengthM = AnalysisOptions.persistenceLengthM(line, persistenceLengthNmOption);
    double lastResortMinimumForcePn = Double.parseDouble(line.getOptionValue(lastResortMinimumForcePnOption.getOpt()));
    double forceRangePiezoStepNm = Double.parseDouble(line.getOptionValue(forceRangePiezoStepNmOption.getOpt()));
    double preserveRampSizePiezoStepNm = Double.parseDouble(line.getOptionValue(preserveRampSizePiezoStepNmOption.getOpt()));
    int numberOfSamples = Integer.parseInt(line.getOptionValue(numberOfSamplesOption.getOpt()));
//    double baselineFraction = Double.parseDouble(line.getOptionValue(baselineFractionOption.getOpt()));
//    Preconditions.checkArgument(baselineFraction <= 1);
    double hoverForFullRefoldingMillis = Double.parseDouble(line.getOptionValue(hoverForFullRefoldingMillisOption.getOpt()));
    double[] hoverForSomeRefoldingMillis = CommonThings.parseDoubles(line.getOptionValue(
      hoverForSomeRefoldingMillisOption.getOpt()).split(",\\s*"));
    double moleculeTimeLimitMillis = Double.parseDouble(line.getOptionValue(moleculeTimeLimitMillisOption.getOpt()));
    Preconditions.checkArgument(hoverForSomeRefoldingMillis.length != 0);
    boolean forceRecord = commonRefoldingOptions.isForceRecord(line);
    boolean asyncJpCpsPsi = Boolean.parseBoolean(line.getOptionValue(asyncJpCpsPsiOption.getOpt()));
    String driverTypeName = line.getOptionValue(driverTypeOption.getOpt());
    PreserveRampSizeStrategy preserveRampSize = PreserveRampSizeStrategy.parse(line.getOptionValue(preserveRampSizeOption.getOpt()));
    Path junctionPointsDir = JpHelper.createJpDirIfNeeded(junctionPointsDirOption, line);
    Path contactPointAndSlopeDir = ContactPointAndSlopeHelper.createCpsDirIfNeeded(
      contactPointAndSlopeDirOption, line);
    String peakSlopesDirString = line.getOptionValue(peakSlopesDirOption.getOpt());
    Path peakSlopesDir = Paths.get(peakSlopesDirString);
    CommonThings.createDirectoryPathIfNeeded(peakSlopesDir);

    boolean debug = commonRefoldingOptions.isDebug(line);
    PathMatcher debugPathMatcher = targetDir.getFileSystem().getPathMatcher(commonRefoldingOptions.getDebugPattern(line));
    double clRangeStartNm = commonClRefoldingOptions.getClRangeStartNm(line);
    double clRangeEndNm = commonClRefoldingOptions.getClRangeEndNm(line);
    double endOfCurveRangeNm = Double.parseDouble(line.getOptionValue(endOfCurveRangeNmOption.getOpt()));
    double xOffsetStdDevM = Double.parseDouble(line.getOptionValue(xOffsetStdDevNmOption.getOpt())) / 1e9;
    long xOffsetChangeFrequency = Long.parseLong(line.getOptionValue(xOffsetChangeFrequencyOption.getOpt()));
    int maxRequiredWellSpacedAtEnd = Integer.parseInt(line.getOptionValue(maxRequiredWellSpacedAtEndOption.getOpt()));
    boolean equalTime = Boolean.parseBoolean(line.getOptionValue(equalTimeOption.getOpt()).toLowerCase());
    boolean syncRefoldApproachRampSize = Boolean.parseBoolean(line.getOptionValue(syncRefoldApproachRampSizeOption.getOpt()).toLowerCase());
    double updateRefoldMillis = Double.parseDouble(line.getOptionValue(updateRefoldMillisOption.getOpt()));
    double moleculeTimeLimitS = moleculeTimeLimitMillis / 1e3;
    String revVelsParam = line.getOptionValue(reverseVelocitiesNmPerSOption.getOpt());
    double[] revVels = ApproachRetractionAssistant.parseReverseVelocities(revVelsParam);

    Preconditions.checkArgument(maxRequiredWellSpacedAtEnd >= 0);
    if (maxRequiredWellSpacedAtEnd == 0) {
      logger.warning("It's very likely to get stuck in a refold loop.");
    }
    int allowSkippedFetch = Integer.parseInt(line.getOptionValue(allowSkippedFetchOption.getOpt()));
    int allowSkippedRefold = Integer.parseInt(line.getOptionValue(allowSkippedRefoldOption.getOpt()));
    int maxIterations = Integer.parseInt(line.getOptionValue(
      maxIterationsOption.getOpt(), Integer.toString(Integer.MAX_VALUE)));
    int backgroundThreads = Integer.parseInt(line.getOptionValue(backgroundThreadsOption.getOpt()));
    String svgDirString = line.getOptionValue(svgDirOption.getOpt());
    Path svgDir = StringUtils.isEmpty(svgDirString) ? null : Paths.get(svgDirString);
    if (svgDir != null) {
      CommonThings.createDirectoryPathIfNeeded(svgDir);
    }

    // Kind of optional.
    Preconditions.checkArgument(breakAwayRampSizeNm >= fetchMoleculeRampSizeNm);
    Preconditions.checkArgument(forceRangePiezoStepNm >= 0);
    Preconditions.checkArgument(preserveRampSizePiezoStepNm >= 0);
    
        RefoldingUtil.checkCaptureDirIsEmpty(captureDir);
    CommonThings.createDirectoryPathIfNeeded(targetDir);

    double clRangeStartM = clRangeStartNm / 1e9, clRangeEndM = clRangeEndNm / 1e9;
    double endOfCurveRangeM = endOfCurveRangeNm / 1e9;

    double pushM = pushNm / 1e9;
    double maxRectificationM = maxRectificationNm / 1e9;
    double hoverM = hoverNm / 1e9;
    double parkingHoverM = parkingHoverNm / 1e9;
    double refoldApproachVelMPerS = refoldApproachVelNmPerS / 1e9;
    double linkerPerModuleM = linkerPerModuleNm / 1e9;
    double fetchMoleculeRampSizeM = fetchMoleculeRampSizeNm / 1e9;
    double defaultParkingRampSizeM = fetchMoleculeRampSizeM - parkingHoverM - pushM;
    Preconditions.checkArgument(defaultParkingRampSizeM > 0);

    double breakAwayRampSizeM = breakAwayRampSizeNm / 1e9;
    double loseRampSizeM = loseRampSizeNm / 1e9;
    double refoldApproachRampSizeM = refoldApproachRampSizeNm / 1e9;
    double loseVelocityMPerS = loseVelocityNmPerS / 1e9;
    double parkingVelocityMPerS = parkingVelocityNmPerS / 1e9;
    final Function<? super Double, Double> minimumPeakForceN = AnalysisOptions.minPeakForceN(
      line, minPeakForcePnOption);
    double extendedForceRangeStartN = extendedForceRangeStartPn / 1e12;
    double extendedForceRangeEndN = extendedForceRangeEndPn / 1e12;
    double lastResortMinimumForceN = lastResortMinimumForcePn / 1e12;
    double forceRangePiezoStep = forceRangePiezoStepNm / 1e9;
    double preserveRampSizePiezoStep = preserveRampSizePiezoStepNm / 1e9;
    double hoverForFullRefoldingS = hoverForFullRefoldingMillis / 1e3;
    double[] hoverForSomeRefoldingS = CommonThings.multiply(hoverForSomeRefoldingMillis, 1e-3);

    double fetchMoleculeSurfaceDelayS = fetchMoleculeSurfaceDelayMillis / 1e3;
    double updateRefoldS = updateRefoldMillis / 1e3;

    // NanoDriver sleeper is not allowed here.
    Sleeper sleeper = Sleeper.SYSTEM_SLEEPER;
    FileMovingWatcher fileMovingWatcher = new FileMovingWatcher(
      waitWhileEmptyMillis, sleepMillis, sleeper, captureDir, targetDir);
    Sleeper nanoDriverSleeper = commonRefoldingOptions.getNanoDriverSleeper(line).make(null);

    DriverType type = DriverType.of(driverTypeName);
    double initialRefoldRetractionRampSizeM = fetchMoleculeRampSizeM - hoverM - pushM;
    final RefoldingNanoAdapter adapter;
    switch (type) {
    case STRIP_CHART:
      adapter = new StripChartRefoldingNanoAdapter(
        revVels, breakAwayRampSizeM, fetchMoleculeRampSizeM, fetchMoleculeSurfaceDelayS, pushM,
        hoverM, parkingHoverM, refoldApproachVelMPerS, loopCount, loseRampSizeM, loseVelocityMPerS,
        parkingVelocityMPerS, allowedTimeRelativeError, refoldAllowedTimeRelativeError, asyncJpCpsPsi ? null
          : junctionPointsDir, sleeper, nanoDriverSleeper, fileMovingWatcher);
      break;
    case PLAIN_REFOLDING:
      Preconditions.checkArgument(refoldApproachRampSizeM == initialRefoldRetractionRampSizeM);
      Preconditions.checkArgument(loopCount == 1, "loopCount={0}", loopCount);
      adapter = new PlainRefoldingNanoAdapter(
        nanoscopeVersion, numberOfSamples, revVels, breakAwayRampSizeM, fetchMoleculeRampSizeM,
        fetchMoleculeSurfaceDelayS, pushM, hoverM, parkingHoverM, loseRampSizeM, loseVelocityMPerS,
        allowedTimeRelativeError, asyncJpCpsPsi ? null : junctionPointsDir, sleeper, nanoDriverSleeper, fileMovingWatcher);
      break;
    default:
      throw new IllegalArgumentException("type=" + type);
    }
    NanoDriverConfig config = PicoUtil.isFakeOrRemoteNanoDriver(nanoDriverClassname) ? null
      : adapter.createRobotConfig(forceRecord);
    NanoDriver nanoDriver = DelayedNanoDriverInvocationHandler.wrapIfNecessary(
      PicoUtil.nanoDriver(nanoDriverClassname, config, nanoDriverHost, nanoDriverPort),
      nanoDriverDelayMillis);
    adapter.setNanoDriver(nanoDriver);

    RefoldingTime refoldingTime = RefoldingTime.create(equalTime, Math.max(revVels.length, 1)
      * hoverForSomeRefoldingS.length, updateRefoldS);

    Random rnd = ThreadLocalRandom.current();
    Executor executor = backgroundThreads == 0 ? CommonThings.callerRunsExecutor
      : Executors.newFixedThreadPool(
        backgroundThreads,
        new BasicThreadFactory.Builder().namingPattern("background-%d").daemon(true).priority(
          Thread.MIN_PRIORITY).build());

    double kbtOverP = Wlc.KB * temperatureK / persistenceLengthM;
    AfmState latest = new AfmState();
    latest.refoldingTime = refoldingTime;
    State state = State.MUST_BREAK_AWAY;
    int breakAwayCount = 0;
    for (int i = lowestAvailableIteration(targetDir); i < maxIterations; ++i) {
      try {
        switch (state) {
        case MUST_BREAK_AWAY:
          if (breakAwayCount++ % xOffsetChangeFrequency == 0 && xOffsetStdDevM != 0
            && latest != null) {
            double xOffsetM = PicoUtil.generateXOffset(latest.xOffsetM, xOffsetStdDevM, rnd);
            adapter.xOffsetM(xOffsetM);
          }
          state = breakAway(
            asyncJpCpsPsi, approachSkipDistanceM, slope, breakAwayRampSizeM, pushM,
            junctionPointsDir, contactPointAndSlopeDir, peakSlopesDir, parkingHoverM, minimumPeakForceN,
            minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
            leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
            rightMuchHigherThanAllN, rightMuchHigherN,
            atMostOnePeakWindowM, kbtOverP, i, debug, debugPathMatcher, adapter,
            svgIterationOffsetN, svgDefScale, svgDir, latest,
            executor);
          break;
        case FETCH_MOLECULE:
          state = fetchMolecule(
            asyncJpCpsPsi, minimumPeakForceN, minPeakForceRelativeToEmpiricalBaselineN,
            medianAbsoluteDeviationLowerBoundN,
            leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
            rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM,
            fetchMoleculeRampSizeM, pushM, maxRectificationM, junctionPointsDir, contactPointAndSlopeDir,
            peakSlopesDir, clRangeStartM, clRangeEndM, kbtOverP, allowSkippedFetch,
            maxRequiredWellSpacedAtEnd, linkerPerModuleM, parkingHoverM, i, debug, debugPathMatcher, adapter,
            svgIterationOffsetN, svgDefScale, svgDir, latest, executor);
          Preconditions.checkState(state == State.MUST_BREAK_AWAY || state == State.SOME_REFOLDING);
          break;
        case FULL_REFOLDING:
          state = refold(
            asyncJpCpsPsi, lastResortMinimumForceN, extendedForceRangeStartN, extendedForceRangeEndN,
            minimumPeakForceN, minPeakForceRelativeToEmpiricalBaselineN,
            medianAbsoluteDeviationLowerBoundN,
            leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
            rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM, forceRangePiezoStep,
            preserveRampSizePiezoStep, hoverForFullRefoldingS, hoverForSomeRefoldingS, hoverM,
            clRangeStartM, clRangeEndM, kbtOverP, endOfCurveRangeM, preserveRampSize,
            allowSkippedRefold, maxRequiredWellSpacedAtEnd, linkerPerModuleM, parkingHoverM,
            refoldApproachRampSizeM, syncRefoldApproachRampSize, initialRefoldRetractionRampSizeM,
            moleculeTimeLimitS, i, debug, debugPathMatcher, junctionPointsDir, contactPointAndSlopeDir, peakSlopesDir,
            svgIterationOffsetN, svgDefScale, svgDir, adapter, latest, executor);
          break;
        case SOME_REFOLDING:
          state = refold(
            asyncJpCpsPsi, Double.NaN, extendedForceRangeStartN, extendedForceRangeEndN,
            minimumPeakForceN, minPeakForceRelativeToEmpiricalBaselineN,
            medianAbsoluteDeviationLowerBoundN,
            leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
            rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM, forceRangePiezoStep,
            preserveRampSizePiezoStep, Double.NaN, hoverForSomeRefoldingS, hoverM, clRangeStartM,
            clRangeEndM, kbtOverP, endOfCurveRangeM, preserveRampSize, allowSkippedRefold,
            maxRequiredWellSpacedAtEnd, linkerPerModuleM, parkingHoverM,
            refoldApproachRampSizeM, syncRefoldApproachRampSize, initialRefoldRetractionRampSizeM,
            moleculeTimeLimitS, i,
            debug, debugPathMatcher, junctionPointsDir, contactPointAndSlopeDir, peakSlopesDir,
            svgIterationOffsetN, svgDefScale, svgDir, adapter,
            latest, executor);
          break;
        default:
          throw new MyRuntimeException("impossible");
        }
      } catch (RuntimeException ex) {
        logger.log(Level.WARNING, "msg={0} stackTrace={1}", new Object[] {
        ex.getMessage(), ExceptionUtils.getFullStackTrace(ex)});
        String prefix = intToNiceString6(i) + "_";
        String suffix = "_cleanup";
        fileMovingWatcher.moveAllFilesFromCaptureToTarget(prefix, suffix);
        latest.prepareForBreakAway();
        state = State.MUST_BREAK_AWAY;
      }
    }
  }

  private static void submitSvg(
    final Parsed parsed, final List<RedundantPicoCurveInfo> a, final double svgIterationOffsetN,
    final double svgDefScale, final Path svgPath, final Executor executor) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        saveSvgQuietly(parsed, a, svgIterationOffsetN, svgDefScale, svgPath);
      }
    });
  }

  private static void saveSvgQuietly(
    Parsed parsed, List<RedundantPicoCurveInfo> a, double svgIterationOffsetN,
    double svgDefScale, final Path svgPath) {
    try {
      Map<Color, List<Shape>> color2shapes = BatchApproachRetractionAnalysis.draw(
        parsed, a, svgIterationOffsetN, svgDefScale);
      Images.saveSvg(color2shapes, svgPath);
    } catch (RuntimeException t) {
      logger.log(Level.WARNING, "Could not save svg file {0} because {1}.", new Object[] {
      svgPath, t});
    }
  }

  private static int lowestAvailableIteration(Path targetDir) {
    Pattern pattern = Pattern.compile("^(\\d{6})_.*$");
    int i = 0;
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(targetDir)) {
      for (Path path : stream) {
        Matcher matcher = pattern.matcher(path.getFileName().toString());
        // There should be no strange files in the target dir.
        Preconditions.checkState(
          matcher.matches(), "path=%s does not match pattern=%s", path, pattern);
        int j = Integer.parseInt(matcher.group(1));
        i = Math.max(i, j + 1);
      }
    } catch (IOException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
    logger.log(Level.INFO, "Starting index: {0}", i);
    return i;
  }

  private static String intToNiceString6(int i) {
    return String.format(Locale.ROOT, "%06d", i);
  }

  private static double keepReasonable(double d, double l) {
    double x = Math.min(l, Math.max(-l, d));
    if (x != d) {
      logger.log(Level.WARNING, "Moving {0} nm instead of {1} nm.", new Object[] {x * 1e9, d * 1e9});
    }
    return x;
  }

  /**
   * Waits a lot, and if no modules refold, it means the molecule was lost, so
   * it goes back to BreakAway. Otherwise, sets the RampSize according to the
   * contour length of the last peak. In the future, we could actually look at
   * the last data point.
   * @param hoverM
   * @param clRangeStartM
   * @param clRangeEndM
   * @param debug
   * @param cpsDir
   * @param nanoAdapter
   * @param latest
   * @param refoldingRampSizeM
   * @param pushM
   *          Only used the first time.
   * 
   * @return
   */
  private static State refold(
    boolean asyncJpCpsPsi, double lastResortMinimumForceN, double extendedForceRangeStartN, double extendedForceRangeEndN,
    Function<? super Double, Double> minimumPeakForceN,
    double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double forceRangePiezoStep, double preserveRampSizePiezoStep, double fullyRefoldSurfaceDelayS,
    double[] refoldSurfaceDelayS, double hoverM, double clRangeStartM, double clRangeEndM,
    double kbtOverP, double endOfCurveRangeM, PreserveRampSizeStrategy preserveRampSizeStrategy,
    int allowSkippedFetch, int maxRequiredWellSpacedAtEnd, double linkerPerModuleM,
    double parkingHoverM,
    double refoldApproachRampSizeM, boolean syncRefoldApproachRampSize, double initialRefoldRetractionRampSizeM,
    double moleculeTimeLimitS, int iteration, boolean debug, PathMatcher debugPathMatcher, Path jpDir, Path cpsDir,
    Path peakSlopesDir, double svgIterationOffsetN, double svgDefScale,
    Path svgDir, RefoldingNanoAdapter nanoAdapter,
    AfmState latest, Executor executor) {
    latest.reachedDesiredForceRange |= extendedForceRangeStartN <= latest.maximumForceLastModuleN
      & latest.maximumForceLastModuleN <= extendedForceRangeEndN;
    double extensionM;
    boolean maximumForceLastModuleIsControlled = extendedForceRangeStartN != 0
      | extendedForceRangeEndN != Double.POSITIVE_INFINITY;
    if (maximumForceLastModuleIsControlled) {
      double desiredForce = (extendedForceRangeStartN + extendedForceRangeEndN) / 2;
      double relevantForceRangePiezoStep = latest.reachedDesiredForceRange ? preserveRampSizePiezoStep
        : forceRangePiezoStep;
      if (relevantForceRangePiezoStep == 0) {
        /*
         * lastPeakExtensionM is NaN when maxRequiredPeaks is 0 and no peaks
         * were found during fetch.
         */
        if (!Double.isNaN(latest.lastPeakExtensionM)) {
          final double contourLength;
          if (latest.lastPeakExtensionM >= latest.refoldingExtensionM - 3e-9) {
            contourLength = latest.lastPeakContourLengthM;
          } else {
            logger.log(
              Level.INFO,
              "Assuming one peak is missing because lastPeakExtensionNm={0} and refoldingExtensionNm={1}.",
              new Object[] {latest.lastPeakExtensionM * 1e9, latest.refoldingExtensionM * 1e9});
            contourLength = latest.lastPeakContourLengthM + clRangeStartM;
          }

          double actualDistanceM = Wlc.computeEndToEndDistance(
            kbtOverP, latest.lastPeakContourLengthM, latest.lastPeakForceN);
          double latestWlcExtensionM = actualDistanceM + latest.lastPeakForceN / latest.k;

          double factor = latest.lastPeakExtensionM / latestWlcExtensionM;

          double distanceM = Wlc.computeEndToEndDistance(kbtOverP, contourLength, desiredForce);
          double wlcExtensionM = distanceM + desiredForce / latest.k;

          double diffM = wlcExtensionM - latest.refoldingExtensionM / factor;
          // Don't change more than half a module at a time.
          double moduleM = (clRangeStartM + clRangeEndM) / 2;
          double safeDiffM = Math.min(moduleM / 2, Math.max(-moduleM / 2, diffM));
          double safeExtensionM = latest.refoldingExtensionM + safeDiffM * factor;

          logger.log(
            Level.INFO,
            "oldExtensionNm={0} oldPeakExtensionNm={1} oldWlcExtensionNm={2} factor={3} distanceNm={4} wlcExtensionNm={5} diffNm={6} safeExtensionNm={7}",
            new Object[] {
            latest.refoldingExtensionM * 1e9, latest.lastPeakExtensionM * 1e9,
            latestWlcExtensionM * 1e9, factor, distanceM * 1e9, wlcExtensionM * 1e9, diffM * 1e9,
            safeExtensionM * 1e9});
          extensionM = safeExtensionM;
        } else {
          extensionM = latest.refoldingExtensionM;
        }
      } else {
        extensionM = latest.refoldingExtensionM
          + Math.signum(desiredForce - latest.maximumForceLastModuleN)
          * relevantForceRangePiezoStep;
      }
    } else {
      extensionM = latest.refoldingExtensionM;
    }
    logger.log(Level.INFO, "oldExtensionNm={0} extensionNm={1}", new Object[] {
    latest.refoldingExtensionM * 1e9, extensionM * 1e9});
    final boolean preserveRampSize;
    switch (preserveRampSizeStrategy) {
    case AFTER:
      preserveRampSize = latest.reachedDesiredForceRange;
      break;
    case ALWAYS:
      preserveRampSize = true;
      break;
    case NEVER:
      preserveRampSize = false;
    default:
      throw new MyRuntimeException("impossible");
    }
    logger.log(Level.INFO, "preserveRampSize={0}", latest.reachedDesiredForceRange);
    /*
     * Assume that all shifts are due to piezo drift, all the time, even if we
     * know that not to be the case before reaching the desired force.
     */
    final double retractionRampSizeM, parkingDeltaM;
    if (preserveRampSize) {
      parkingDeltaM = latest.parkingM + latest.refoldingExtensionM - extensionM;
      // Do not change ramp size.
      retractionRampSizeM = latest.refoldingExtensionM - hoverM;
      /*
       * Do not change extension so that later the line
       * latest.refoldingExtensionM = extensionM doesn't change it either
       * (back).
       */
      extensionM = latest.refoldingExtensionM;
    } else {
      retractionRampSizeM = extensionM - hoverM;
      parkingDeltaM = latest.parkingM;
    }
    latest.refoldingTime.updateTimeVelIndexAndStartTracking();
    ContactPointsAndSlope contactPointsAndSlope = latest.contactPointsAndSlope;
    final State nextState;

    double timeSinceFetchS = latest.refoldingTime.timeSinceFetchS();
    if (timeSinceFetchS > moleculeTimeLimitS) {
      logger.log(
        Level.INFO, "Losing molecule because timeSinceFetch(ms)={0}.",
        timeSinceFetchS * 1e3);
      PreviousAndCurrentLists refoldingResults = nanoAdapter.lose(
        syncRefoldApproachRampSize ? Math.max(0, refoldApproachRampSizeM + (retractionRampSizeM - initialRefoldRetractionRampSizeM)) : refoldApproachRampSizeM,
        retractionRampSizeM, parkingDeltaM, intToNiceString6(iteration) + "_", "_lose");
      if (asyncJpCpsPsi) {
        submitJpFiles(refoldingResults, jpDir, executor);
      }
      if (debug) {
        analysePreviousRefolding(
          asyncJpCpsPsi, iteration, refoldingResults.previous, contactPointsAndSlope, minimumPeakForceN,
          minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
          leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
          rightMuchHigherThanAllN, rightMuchHigherN,
          atMostOnePeakWindowM, kbtOverP, debugPathMatcher, cpsDir, peakSlopesDir,
          svgDefScale, svgIterationOffsetN, svgDir, executor);
      }
      Preconditions.checkState(refoldingResults.current.size() <= 1);
      Preconditions.checkState(refoldingResults.unparseable.size()
        + refoldingResults.current.size() >= 1);
      if (debug) {
        for (ParsedAndTargetFile c : refoldingResults.current) {
          if (debugPathMatcher.matches(c.targetFile)) {
            analyseAndSubmitSvg(
              asyncJpCpsPsi, c,
              intToNiceString6(iteration - 1) + "_",
              contactPointsAndSlope,
              minimumPeakForceN,
              minPeakForceRelativeToEmpiricalBaselineN,
              medianAbsoluteDeviationLowerBoundN,
              leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
              rightMuchHigherThanAllN,
              rightMuchHigherN,
              atMostOnePeakWindowM, kbtOverP, cpsDir, peakSlopesDir,
              svgDefScale, svgIterationOffsetN, svgDir, executor);
          }
        }
      }
      nextState = State.MUST_BREAK_AWAY;
    } else {
      boolean fullyRefold = !Double.isNaN(fullyRefoldSurfaceDelayS);
      Preconditions.checkArgument(!Double.isNaN(lastResortMinimumForceN) == fullyRefold);
      /*
       * For fullyRefold, keep the same parameters to see what peaks would look
       * like at that retraction speed.
       */
      double surfaceDelayS = Double.isNaN(fullyRefoldSurfaceDelayS) ? refoldSurfaceDelayS[latest.refoldingTime.getIndex()
        % refoldSurfaceDelayS.length]
        : fullyRefoldSurfaceDelayS;
      String prefix = intToNiceString6(iteration) + "_";
      String suffix = "_refold_" + fullyRefold + "_" + surfaceDelayS * 1e3 + "_" + retractionRampSizeM * 1e9;
      PreviousAndCurrentLists refoldingResults = nanoAdapter.refold(latest.refoldingTime.getIndex()
        / refoldSurfaceDelayS.length, parkingDeltaM,
        syncRefoldApproachRampSize ? Math.max(0, refoldApproachRampSizeM + (retractionRampSizeM - initialRefoldRetractionRampSizeM)) : refoldApproachRampSizeM,
        retractionRampSizeM, surfaceDelayS, prefix, suffix);
      if (asyncJpCpsPsi) {
        submitJpFiles(refoldingResults, jpDir, executor);
      }
      Preconditions.checkState(refoldingResults.size() >= 1);
      if (debug) {
        analysePreviousRefolding(
          asyncJpCpsPsi, iteration, refoldingResults.previous, contactPointsAndSlope, minimumPeakForceN,
          minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
          leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
          rightMuchHigherThanAllN, rightMuchHigherN,
          atMostOnePeakWindowM, kbtOverP, debugPathMatcher, cpsDir, peakSlopesDir,
          svgDefScale, svgIterationOffsetN, svgDir, executor);
      }

      List<ParsedAndTargetFile> timeSortedCurrent = new ArrayList<>(refoldingResults.current);
      Collections.sort(timeSortedCurrent, new Comparator<ParsedAndTargetFile>() {
        @Override
        public int compare(ParsedAndTargetFile o1, ParsedAndTargetFile o2) {
          /*
           * The Nanoscope files are time-ordered by name. Since the prefix is
           * the same for "current", the time-order is preserved by simply
           * ordering by name.
           */
          return o1.targetFile.getFileName().toString().compareTo(
            o2.targetFile.getFileName().toString());
        }
      });

      List<State> states = new ArrayList<>();
      for (Iterator<ParsedAndTargetFile> it = timeSortedCurrent.iterator(); it.hasNext();) {
        ParsedAndTargetFile refoldingResult = it.next();
        List<RedundantPicoCurveInfo> a = analyseAndSubmitSvg(
          asyncJpCpsPsi, refoldingResult, prefix, contactPointsAndSlope, minimumPeakForceN, minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN, leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM, kbtOverP, cpsDir, peakSlopesDir, svgDefScale, svgIterationOffsetN, svgDir, executor);
        double maximumForceLastModuleN = extractMaximumForceEndOfCurve(
          refoldingResult.last(), CommonThings.last(a), endOfCurveRangeM);
        logger.log(Level.FINE, "maximumForceLastModulePn={0}", maximumForceLastModuleN * 1e12);
        if (!it.hasNext()) {
          if (CommonThings.last(a).peakCount() != 0) {
            updateLastPeak(refoldingResult.last(), CommonThings.last(a), latest);
          }
          // If no peaks are found, keep the latest last peak spec.
          latest.maximumForceLastModuleN = maximumForceLastModuleN;
        }
        logger.log(Level.INFO, "{0}: last iteration {1} has {2} peaks.", new Object[] {
        refoldingResult.targetFile.getFileName(), a.size() - 1, CommonThings.last(a).peakCount()});
        final State state;
        if (fullyRefold) {
          int minPeaks = RefoldingUtil.minPeaks(
            retractionRampSizeM, clRangeStartM, clRangeEndM, 0, linkerPerModuleM);
          minPeaks = Math.min(minPeaks, maxRequiredWellSpacedAtEnd);
          int wellSpacedAtEnd = PicoDataAnalyser.wellSpacedAtEnd(
            CommonThings.last(a).peakContourLengthsFromContactPointOnwards(), clRangeStartM, clRangeEndM,
            allowSkippedFetch);
          logger.log(Level.INFO, "minPeaks={0} wellSpacedAtEnd={1}", new Object[] {
          minPeaks, wellSpacedAtEnd});
          if ((wellSpacedAtEnd >= minPeaks & (CommonThings.last(a).peakCount() == 0 || CommonThings.last(a).lastPeakOrNull().nonNegativeContourLengthMetres
            + (1 + allowSkippedFetch) * clRangeEndM >= extensionM))
            | (maximumForceLastModuleN >= lastResortMinimumForceN)) {
            state = State.SOME_REFOLDING;
          } else {
            state = State.MUST_BREAK_AWAY;
          }
        } else {
          final boolean stay;
          // TODO: Only add the Rate as a parameter.
          if (maximumForceLastModuleIsControlled) {
            stay = extendedForceRangeStartN <= maximumForceLastModuleN
              & maximumForceLastModuleN <= extendedForceRangeEndN;
          } else {
            stay = CommonThings.last(a).peakCount() != 0;
          }
          state = stay ? State.SOME_REFOLDING : State.FULL_REFOLDING;
        }
        states.add(state);
      }

      if (states.contains(State.SOME_REFOLDING)) {
        nextState = State.SOME_REFOLDING;
      } else if (states.contains(State.FULL_REFOLDING) | states.isEmpty()) {
        // If we couldn't parse anything (states.isEmpty()), do full
        // refolding.
        nextState = State.FULL_REFOLDING;
      } else {
        nextState = State.MUST_BREAK_AWAY;
      }
    }
    latest.parkingM = parkingDeltaM;
    /*
     * Only update latest if all processing went well.
     */
    latest.refoldingExtensionM = extensionM;
    return nextState;
  }

  static RedundantPicoCurveInfo analyse(
    ContactPointsAndSlope contactPointsAndSlope, PlainPicoData plainPicoData,
    double baselineFraction, double minimumPeakForceN,
    double minPeakForceRelativeToEmpiricalBaselineN, double medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    double rightMuchHigherThanAllN, double rightMuchHigherN,
    double atMostOnePeakWindowM, double kbtOverP, String filename, Path cpsDir, Path peakSlopesDir) {
    final double empiricalRetractionContactPointDeflection;
    if (baselineFraction < 0) {
      empiricalRetractionContactPointDeflection = contactPointsAndSlope.retractionContactPoint.deflectionLsb;
    } else {
      empiricalRetractionContactPointDeflection = PicoDataAnalyser.findEmpiricalRetractionContactPointDefLsbGivenZLsbAndSlope(
        plainPicoData.sharedRetraction(), contactPointsAndSlope.retractionContactPoint.zLsb,
        contactPointsAndSlope.slopeZLsbDefLsb, baselineFraction);
    }
    PeakAnalysisInput input;
    try {
      input = new PeakAnalysisInput(
        plainPicoData,
        contactPointsAndSlope.withRetractionContactPointAndSlope(ContactPointAndSlopeHelper.readOrWriteContactPointAndSlope(
          new ContactPointAndSlope(
            contactPointsAndSlope.retractionContactPoint.zLsb,
            empiricalRetractionContactPointDeflection, contactPointsAndSlope.slopeZLsbDefLsb),
          filename, cpsDir)), minimumPeakForceN, PicoDataAnalyser.defaultMinimumPeakContourLengthM,
        PicoDataAnalyser.defaultMinContourLengthDeltaM, minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN,
        leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
        rightMuchHigherThanAllN,
        rightMuchHigherN, atMostOnePeakWindowM, kbtOverP);
    } catch (IOException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
    RedundantPicoCurveInfo analysis = PicoDataAnalyser.analyseWithGivenContactPointsAndSlope(
      input, filename, peakSlopesDir);
    return analysis;
  }

  private static void analysePreviousRefolding(
    boolean asyncJpCpsPsi, int iteration, List<ParsedAndTargetFile> previous,
    ContactPointsAndSlope contactPointsAndSlope, Function<? super Double, Double> minimumPeakForceN,
    double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double kbtOverP, PathMatcher debugPathMatcher, Path cpsDir, Path peakSlopesDir,
    double svgDefScale, double svgIterationOffsetN,
    Path svgDir, Executor executor) {
    for (ParsedAndTargetFile refoldingResult : previous) {
      if (debugPathMatcher.matches(refoldingResult.targetFile)) {
        analyseAndSubmitSvg(
          asyncJpCpsPsi, refoldingResult, intToNiceString6(iteration - 1) + "_",
          contactPointsAndSlope, minimumPeakForceN, minPeakForceRelativeToEmpiricalBaselineN,
          medianAbsoluteDeviationLowerBoundN, leftMostlyHigherWindowSizeM,
          leftMostlyHigherFraction, leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN,
          atMostOnePeakWindowM, kbtOverP, cpsDir, peakSlopesDir, svgDefScale, svgIterationOffsetN, svgDir, executor);
      }
    }
  }

  private static List<RedundantPicoCurveInfo> analyseAndSubmitSvg(
    boolean asyncCpsPsi, ParsedAndTargetFile refoldingResult, String prefix, ContactPointsAndSlope contactPointsAndSlope,
    Function<? super Double, Double> minimumPeakForceN,
    double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double kbtOverP, Path cpsDir, Path peakSlopesDir, double svgDefScale, double svgIterationOffsetN,
    Path svgDir, Executor executor) {
    try {
      return coreAnalyseAndSubmitSvg(asyncCpsPsi, refoldingResult, prefix, contactPointsAndSlope,
        minimumPeakForceN,
        minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN,
        leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
        rightMuchHigherThanAllN,
        rightMuchHigherN, atMostOnePeakWindowM,
        kbtOverP, cpsDir, peakSlopesDir, svgDefScale, svgIterationOffsetN, svgDir,
        executor);
    } catch (IOException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
  }
  
  private static List<RedundantPicoCurveInfo> coreAnalyseAndSubmitSvg(
    boolean asyncCpsPsi, ParsedAndTargetFile refoldingResult, String prefix, ContactPointsAndSlope contactPointsAndSlope,
    Function<? super Double, Double> minimumPeakForceN,
    double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double kbtOverP, Path cpsDir, Path peakSlopesDir,
    double svgDefScale, double svgIterationOffsetN,
    Path svgDir, Executor executor) throws IOException {
    Path contactPointAndSlopeFile = ContactPointAndSlopeHelper.contactPointAndSlopeFile(
      cpsDir, refoldingResult.targetFile.getFileName().toString());
    if (asyncCpsPsi) {
      submitCpsFile(
        contactPointsAndSlope.retractionContactPointAndSlope(), contactPointAndSlopeFile, executor);
    } else {
      try {
        contactPointsAndSlope = contactPointsAndSlope.withRetractionContactPointAndSlope(ContactPointAndSlopeHelper.readOrWriteContactPointAndSlope(
          contactPointsAndSlope.retractionContactPointAndSlope(), contactPointAndSlopeFile));
      } catch (IOException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
    }
    Path peakSlopesPath = PeakSlopesHelper.peakSlopesFile(
      peakSlopesDir, refoldingResult.targetFile.getFileName().toString());
    final List<PeakSlopes> peakSlopesList;
    if (asyncCpsPsi) {
      peakSlopesList = null;
    } else {
      try {
        peakSlopesList = (peakSlopesPath != null && Files.isRegularFile(peakSlopesPath)) ? PeakSlopesHelper.extractPeakSlopesList(peakSlopesPath)
          : null;
      } catch (IOException e) {
        throw new MyRuntimeException("Could not read: " + peakSlopesPath, e);
      }
    }
    List<RedundantPicoCurveInfo> a = BatchApproachRetractionAnalysis.analyseWithCpsAndOptionalPeakSlopes(
      minimumPeakForceN, PicoDataAnalyser.defaultMinimumPeakContourLengthM,
      PicoDataAnalyser.defaultMinContourLengthDeltaM,
      minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
      leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN,
      atMostOnePeakWindowM, kbtOverP, refoldingResult.targetFile, refoldingResult.parsed.picoDataList, contactPointsAndSlope, peakSlopesList);
    Preconditions.checkState(peakSlopesList == null || peakSlopesList.size() <= a.size());
    if ((peakSlopesList == null || peakSlopesList.size() != a.size()) & peakSlopesPath != null) {
      List<PeakSlopes> peakSlopes = RedundantPicoCurveInfo.peakSlopes(a);
      if (asyncCpsPsi) {
        submitPsiFile(peakSlopes, peakSlopesPath, executor);
      } else {
        PeakSlopesHelper.writeToFile(peakSlopes, peakSlopesPath);
      }
    }
    if (ENABLE_SVG & svgDir != null) {
      submitSvg(
        refoldingResult.parsed, a, svgIterationOffsetN, svgDefScale,
        svgDir.resolve(prefix
          + refoldingResult.targetFile.getFileName() + ".svg"), executor);
    }
    return a;
  }

  /**
   * @param fetchMoleculeRampSizeM
   *          Although nanoAdapter already knows about it, we also need it here
   *          to compute how many peaks we should require. Perhaps in the future
   *          we'll take it from the actual ramp size from picoData.
   */
  private static State fetchMolecule(
    boolean asyncJpCpsPsi,
    Function<? super Double, Double> minimumPeakForceN, double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double fetchMoleculeRampSizeM, double pushM, double maxRectificationM, Path jpDir, Path cpsDir,
    Path peakSlopesDir, double clRangeStart, double clRangeEnd, double kbtOverP,
    int allowSkippedFetch, int maxRequiredWellSpacedAtEnd, double linkerPerModuleM,
    double parkingHoverM, int iteration, boolean debug, PathMatcher debugPathMatcher, RefoldingNanoAdapter nanoAdapter,
    double svgIterationOffsetN, double svgDefScale, Path svgDir, AfmState latest, Executor executor)
    throws InterruptedException {
    Preconditions.checkState(Double.isNaN(latest.refoldingExtensionM));
    double deltaM = keepReasonable(pushM - latest.getBreakAwayPushingMetres(), maxRectificationM);
    ParsedAndTargetFile picoDataAndTargetFile = nanoAdapter.fetchMolecule(latest.parkingM
      + deltaM, intToNiceString6(iteration) + "_", "_fetch_" + fetchMoleculeRampSizeM * 1e9);
    if (asyncJpCpsPsi) {
      submitJpFile(picoDataAndTargetFile, jpDir, executor);
    }
    latest.parkingM += deltaM;
    // Here we assume that keepReasonable managed to reach pushM.
    latest.setBreakAwayPushingMetres(Double.NaN);
    PlainPicoData fetchMoleculePicoData = picoDataAndTargetFile.emptyOrOneToParsedAndTargetFile().plainPicoData;

    final State state;
    if (fetchMoleculePicoData == null) {
      logger.warning("Could not get latest file.");
      state = State.MUST_BREAK_AWAY;
    } else {
      // For now, we're potentially using the more accurate contact point and
      // slope
      // from the BreakAway curve.
      final ContactPointAndSlope retractionContactPointAndSlope;
      Path contactPointAndSlopeFile = ContactPointAndSlopeHelper.contactPointAndSlopeFile(
        cpsDir, picoDataAndTargetFile.targetFile.getFileName().toString());
      if (asyncJpCpsPsi) {
        retractionContactPointAndSlope = latest.contactPointsAndSlope.retractionContactPointAndSlope();
        submitCpsFile(retractionContactPointAndSlope, contactPointAndSlopeFile, executor);
      } else {
        try {
          retractionContactPointAndSlope = ContactPointAndSlopeHelper.readOrWriteContactPointAndSlope(
            latest.contactPointsAndSlope.retractionContactPointAndSlope(), contactPointAndSlopeFile);
        } catch (IOException e) {
          throw new MyRuntimeException(e.getMessage(), e);
        }
      }
      PeakAnalysisInput input = new PeakAnalysisInput(
        fetchMoleculePicoData,
        latest.contactPointsAndSlope.withRetractionContactPointAndSlope(retractionContactPointAndSlope),
        minimumPeakForceN.apply(fetchMoleculePicoData.retraction.velocity),
        PicoDataAnalyser.defaultMinimumPeakContourLengthM,
        PicoDataAnalyser.defaultMinContourLengthDeltaM,
        minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN.apply(fetchMoleculePicoData.sharedRetraction().velocity),
        leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
        rightMuchHigherThanAllN.apply(fetchMoleculePicoData.sharedRetraction().velocity),
        rightMuchHigherN.apply(fetchMoleculePicoData.sharedRetraction().velocity),
        atMostOnePeakWindowM, kbtOverP);
      
      Path peakSlopesPath = PeakSlopesHelper.peakSlopesFile(
        peakSlopesDir, picoDataAndTargetFile.targetFile.getFileName().toString());
      final PeakSlopes peakSlopes;
      if (asyncJpCpsPsi) {
        peakSlopes = null;
      } else {
        try {
          peakSlopes = (peakSlopesPath != null && Files.isRegularFile(peakSlopesPath)) ? CommonThings.only(PeakSlopesHelper.extractPeakSlopesList(peakSlopesPath))
            : null;
        } catch (IOException e) {
          throw new MyRuntimeException("Could not read: " + peakSlopesPath, e);
        }
      }
      RedundantPicoCurveInfo analysis = PicoDataAnalyser.analyseWithGivenContactPointsAndSlope(input, peakSlopes);
      if (asyncJpCpsPsi) {
        submitPsiFile(ImmutableList.of(analysis.peakSlopes()), peakSlopesPath, executor);
      } else if (peakSlopes == null) {
        try {
          PeakSlopesHelper.writeToFile(ImmutableList.of(analysis.peakSlopes()), peakSlopesPath);
        } catch (IOException e) {
          logger.log(Level.SEVERE, "Could not write: {0}", peakSlopesPath);
        }        
      } else {
        assert peakSlopes == analysis.peakSlopes();
      }

      boolean debugPathMatches = debugPathMatcher.matches(picoDataAndTargetFile.targetFile);
      
      if (ENABLE_SVG & svgDir != null & debug & debugPathMatches) {
        submitSvg(
          picoDataAndTargetFile.parsed,
          ImmutableList.of(analysis),
          svgIterationOffsetN, svgDefScale,
          svgDir.resolve(intToNiceString6(iteration) + "_"
            + picoDataAndTargetFile.targetFile.getFileName().toString() + ".svg"), executor);
      }

      if (debug & debugPathMatches) {
        double actualPushM = ApproachRetractionPush.computeApproachRetractionPush(input).retractionPushingM;
        logger.log(Level.INFO, "actualPushNm={0}", actualPushM * 1e9);
      }
      /*
       * We consider that breakAway is always right about pushing.
       */
      int minPeaks = RefoldingUtil.minPeaksForInterestingFetch(
        fetchMoleculeRampSizeM, pushM, clRangeStart, clRangeEnd, 0, linkerPerModuleM);
      minPeaks = Math.min(minPeaks, maxRequiredWellSpacedAtEnd);
      int wellSpacedAtEnd = PicoDataAnalyser.wellSpacedAtEnd(
        analysis.peakContourLengthsFromContactPointOnwards(), clRangeStart, clRangeEnd,
        allowSkippedFetch);
      short minZRetraction = CommonThings.min(fetchMoleculePicoData.retraction.sharedZArray());
      double actualExtensionM = (retractionContactPointAndSlope.zLsb - minZRetraction)
        * fetchMoleculePicoData.zLsbToMetresFactor;
      double actualApproachRampSizeM = (CommonThings.max(fetchMoleculePicoData.approach.sharedZArray()) - CommonThings.min(fetchMoleculePicoData.approach.sharedZArray()))
        * fetchMoleculePicoData.zLsbToMetresFactor;
      double actualRetractionRampSizeM = (CommonThings.max(fetchMoleculePicoData.retraction.sharedZArray()) - minZRetraction)
        * fetchMoleculePicoData.zLsbToMetresFactor;
      double piezoDriftM = actualExtensionM - (fetchMoleculeRampSizeM - pushM)
        * actualRetractionRampSizeM / fetchMoleculePicoData.rampSizeMetres();
      /*
       * In fact, piezoDriftM .
       */
      logger.log(
        Level.INFO,
        "actualExtensionNm={0} piezoDriftNm={1} actualApproachRampSizeNm={2} actualRetractionRampSizeNm={3}",
        new Object[] {
        actualExtensionM * 1e9, piezoDriftM * 1e9, actualApproachRampSizeM * 1e9,
        actualRetractionRampSizeM * 1e9});
      if (wellSpacedAtEnd >= minPeaks
        & (analysis.peakCount() == 0 || analysis.lastPeakOrNull().nonNegativeContourLengthMetres
          + (1 + allowSkippedFetch) * clRangeEnd >= Math.min(
          fetchMoleculeRampSizeM - pushM, actualExtensionM))) {
        if (ENABLE_SVG & svgDir != null & !(debug & debugPathMatches)) {
          submitSvg(
            picoDataAndTargetFile.parsed,
            ImmutableList.of(analysis),
            svgIterationOffsetN, svgDefScale,
            svgDir.resolve(intToNiceString6(iteration) + "_"
              + picoDataAndTargetFile.targetFile.getFileName().toString() + ".svg"), executor);
        }
        updateEndOfCurveProperties(fetchMoleculePicoData, analysis, clRangeStart, latest);
        latest.refoldingExtensionM = fetchMoleculeRampSizeM - pushM;
        latest.refoldingTime.startTraking();
        state = State.SOME_REFOLDING;
      } else {
        state = State.MUST_BREAK_AWAY;
      }
    }
    return state;
  }

  private static void submitPsiFile(final List<PeakSlopes> peakSlopes, final Path peakSlopesPath, Executor executor) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        try {
          PeakSlopesHelper.writeToFile(peakSlopes, peakSlopesPath);
        } catch (IOException e) {
          logger.log(Level.SEVERE, "Could not write: {0}", peakSlopesPath);
        }        
      }
    });
  }


  private static void submitCpsFile(
    final ContactPointAndSlope cps, final Path path, Executor executor) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        try {
          ContactPointAndSlopeHelper.writeToFile(cps, path);
        } catch (IOException e) {
          logger.log(Level.SEVERE, "Could not write: {0}", path);
        }
      }
    });
  }

  private static void updateEndOfCurveProperties(
    PlainPicoData picoData, RedundantPicoCurveInfo analysis, double endOfCurveRangeM,
    AfmState latest) {
    if (analysis.peakCount() != 0) {
      updateLastPeak(picoData, analysis, latest);
    }
    latest.maximumForceLastModuleN = extractMaximumForceEndOfCurve(
      picoData, analysis, endOfCurveRangeM);
  }

  static void updateLastPeak(
    PlainPicoData picoData, RedundantPicoCurveInfo analysis, AfmState latest) {
    latest.lastPeakContourLengthM = analysis.lastPeakOrNull().nonNegativeContourLengthMetres;
    latest.lastPeakForceN = analysis.forceAtIndexAfterContactPoint(analysis.lastPeakIndex());
    latest.lastPeakExtensionM = (analysis.retractionContactPoint.zLsb - picoData.sharedRetraction().z(
      analysis.offsetAndDeflections.offset + analysis.lastPeakIndex()))
      * picoData.zLsbToMetresFactor;
  }

  private static double extractMaximumForceEndOfCurve(
    PlainPicoData picoData, RedundantPicoCurveInfo analysis, double endOfCurveRangeM) {
    short[] z = picoData.sharedRetraction().sharedZArray();
    int i = z.length;
    while (--i >= analysis.offsetAndDeflections.offset
      && (z[i] - z[z.length - 1]) * picoData.zLsbToMetresFactor < endOfCurveRangeM) {
    }
    if (i == analysis.offsetAndDeflections.offset - 1) {
      logger.log(Level.WARNING, "i={0}", i);
      i = analysis.offsetAndDeflections.offset;
    }
    // Note that the forces are negated, so we take the lowest ones.
    double[] forces = analysis.copyOfForcesNewton();
    Preconditions.checkState(forces.length == picoData.sharedRetraction().timeInstantCount()
      - analysis.offsetAndDeflections.offset);
    Percentile percentile = new Percentile(5);
    int from = i - analysis.offsetAndDeflections.offset;
    int len = z.length - i;
    double negatedMaximumForceN = percentile.evaluate(forces, from, len);
    if (logger.isLoggable(Level.FINEST)) {
      logger.log(
        Level.FINEST, "maximumForcePn={0} min={1} max={2}", new Object[] {
        negatedMaximumForceN * 1e12, CommonThings.min(forces, from, from + len) * 1e12,
        CommonThings.max(forces, from, from + len) * 1e12});
    }
    return -negatedMaximumForceN;
  }

  private static State breakAway(
    boolean asyncJpCpsPsi, double approachSkipDistanceM, double slope, double rampSize, double pushM,
    Path jpDir, Path cpsDir, Path peakSlopesDir, double parkingHoverM,
    Function<? super Double, Double> minimumPeakForceN, double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double kbtOverP, int iteration, boolean debug, PathMatcher debugPathMatcher, RefoldingNanoAdapter nanoAdapter,
    double svgIterationOffsetN, double svgDefScale, Path svgDir, AfmState latest, Executor executor) {
    Preconditions.checkState(Double.isNaN(latest.getBreakAwayPushingMetres()));
    latest.refoldingTime.updateTimeVelIndexAndStartTracking();
    latest.refoldingTime.cancelLatest();
    /*
     * Since I already changed zScanStart, if we fail to get parse the file
     * here, next time we won't change zScanStart again. Otherwise, the actual
     * pushing distance is computed further below.
     */
    PreviousAndCurrentLists results = nanoAdapter.breakAway(
      intToNiceString6(iteration) + "_", "_breakAway_" + rampSize * 1e9);
    if (asyncJpCpsPsi) {
      submitJpFiles(results, jpDir, executor);
    }
    Preconditions.checkState(results.current.size() <= 1);

    if (debug) {
      /*
       * If latest.contactPointsAndSlope is needed here, it means we've
       * "broken away" before.
       */
      analysePreviousRefolding(
        asyncJpCpsPsi, iteration, results.previous, latest.contactPointsAndSlope,
        minimumPeakForceN, minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN,
        leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
        rightMuchHigherThanAllN,
        rightMuchHigherN, atMostOnePeakWindowM, kbtOverP,
        debugPathMatcher,
        cpsDir, peakSlopesDir, svgDefScale, svgIterationOffsetN,
        svgDir, executor);
    }

    final State state;
    if (results.current.isEmpty()) {
      logger.warning("Could not get the latest file.");
      // Stay in the same state
      state = State.MUST_BREAK_AWAY;
    } else {
      PlainPicoDataAndTargetFile picoDataAndTargetFile = CommonThings.last(results.current).emptyOrOneToParsedAndTargetFile();
      PlainPicoData breakAwayPicoData = picoDataAndTargetFile.plainPicoData;

      final ContactPointsAndSlope contactPointsAndSlope;
      try {
        contactPointsAndSlope = ContactPointAndSlopeHelper.readOrComputeAndWriteContactPointAndSlopeSharingSlopeBetweenApproachAndRetraction(
          breakAwayPicoData, approachSkipDistanceM, slope,
          picoDataAndTargetFile.targetFile.getFileName().toString(), cpsDir);
      } catch (IOException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
      boolean debugPathMatches = debugPathMatcher.matches(picoDataAndTargetFile.targetFile);
      if (debug & debugPathMatches) {
        RedundantPicoCurveInfo picoAnalysis = PicoDataAnalyser.analyseWithGivenContactPointsAndSlope(
          breakAwayPicoData, contactPointsAndSlope, PicoDataAnalyser.defaultMinimumPeakForceN,
          PicoDataAnalyser.defaultMinimumPeakContourLengthM,
          PicoDataAnalyser.defaultMinContourLengthDeltaM, minPeakForceRelativeToEmpiricalBaselineN,
          medianAbsoluteDeviationLowerBoundN.apply(breakAwayPicoData.retraction.velocity),
          leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
          rightMuchHigherThanAllN.apply(breakAwayPicoData.retraction.velocity),
          rightMuchHigherN.apply(breakAwayPicoData.retraction.velocity), atMostOnePeakWindowM,
          kbtOverP);
        if (ENABLE_SVG & svgDir != null) {
          submitSvg(
            CommonThings.last(results.current).parsed,
            ImmutableList.of(picoAnalysis),
            svgIterationOffsetN, svgDefScale,
            svgDir.resolve(intToNiceString6(iteration) + "_"
              + picoDataAndTargetFile.targetFile.getFileName().toString() + "_breakAway.svg"),
            executor);
        }
      }
      double actualPushM = ApproachRetractionPush.computeApproachRetractionPush(
        contactPointsAndSlope, breakAwayPicoData).retractionPushingM;
      logger.log(Level.INFO, "actualPushNm={0} slope={1} file={2}", new Object[] {
      actualPushM * 1e9, slope, picoDataAndTargetFile.targetFile.getFileName()});
      latest.setBreakAwayPushingMetres(actualPushM);
      latest.contactPointsAndSlope = contactPointsAndSlope;
      latest.xOffsetM = breakAwayPicoData.xOffsetM;
      latest.k = breakAwayPicoData.springConstantNPerM;
      state = State.FETCH_MOLECULE;
    }
    latest.refoldingExtensionM = Double.NaN;
    latest.lastPeakContourLengthM = Double.NaN;
    latest.lastPeakForceN = Double.NaN;
    latest.maximumForceLastModuleN = Double.NaN;
    latest.lastPeakExtensionM = Double.NaN;
    latest.reachedDesiredForceRange = false;

    // Only the first time.
    return state;
  }

  /**
   * Overwrites if the file is aldready present.
   */
  private static void submitJpFiles(
    final PreviousAndCurrentLists results, final Path jpDir, Executor executor) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        for (final ParsedAndTargetFile pat : Iterables.concat(results.previous, results.current)) {
          if (jpDir != null) {
            Path jpPath = JpHelper.junctionPointsFile(jpDir, pat.targetFile);
            try {
              JpHelper.writeToFile(pat.parsed.jp, jpPath);
            } catch (IOException e) {
              logger.log(Level.SEVERE, "Could not write: {0}", jpPath);
            }
          }
        }
      }
    });
  }
  
  private static void submitJpFile(
    final ParsedAndTargetFile pat, final Path jpDir, Executor executor) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (jpDir != null) {
          Path jpPath = JpHelper.junctionPointsFile(jpDir, pat.targetFile);
          try {
            JpHelper.writeToFile(pat.parsed.jp, jpPath);
          } catch (IOException e) {
            logger.log(Level.SEVERE, "Could not write: {0}", jpPath);
          }
        }
      }
    });
  }

  private static enum State {
    MUST_BREAK_AWAY, FETCH_MOLECULE, FULL_REFOLDING, SOME_REFOLDING
  }

  private static class AfmState {
    public boolean reachedDesiredForceRange;
    public double refoldingExtensionM = Double.NaN;
    public double xOffsetM;
    public double k;
    public double lastPeakContourLengthM;
    /*
     * Needed because extension-based calculations are imprecise because the
     * actual Z movement is on average 96% of what it should be. Piezo drift (5
     * A/s, 30 pN/s) is almost negligible in comparison. Small errors in
     * extension can mean large errors in force. But the diode drift is much
     * smaller (1 pN/s).
     */
    public double lastPeakExtensionM, lastPeakForceN, maximumForceLastModuleN;
    // Read from BreakAway.
    /**
     * Transferred from BreakAway to FetchMolecule only.
     */
    private double breakAwayPushingM = Double.NaN;
    private double parkingM = 0;
    private RefoldingTime refoldingTime;

    // Actually only the retraction part is strictly needed, but whatever.
    private ContactPointsAndSlope contactPointsAndSlope;

    public void prepareForBreakAway() {
      setBreakAwayPushingMetres(Double.NaN);
      refoldingExtensionM = Double.NaN;
    }

    private double getBreakAwayPushingMetres() {
      return breakAwayPushingM;
    }

    private void setBreakAwayPushingMetres(double pushingM) {
      this.breakAwayPushingM = pushingM;
    }
  }

  private static class RefoldingTime {
    final long[] totalNanos;
    final Queue<TotalTimeVelIndex> pq;
    final long updateVelocityNs;
    int index;
    long latestNanoTime, fetchNanoTime;

    private RefoldingTime(long[] totalNanos, Queue<TotalTimeVelIndex> pq, int index,
      long latestNanoTime, long updateVelocityNs) {
      Preconditions.checkArgument(pq == null || totalNanos.length == pq.size() + 1);
      Preconditions.checkArgument((totalNanos.length == 0) == (index == -1));
      this.totalNanos = totalNanos;
      this.pq = pq;
      this.index = index;
      this.latestNanoTime = latestNanoTime;
      this.updateVelocityNs = updateVelocityNs;
    }

    public int getIndex() {
      return index;
    }

    public void startTraking() {
      latestNanoTime = fetchNanoTime = System.nanoTime();
    }

    public double timeSinceFetchS() {
      return (System.nanoTime() - fetchNanoTime) / 1e9;
    }

    public static RefoldingTime create(boolean equalTime, int n, double updateVelocityS) {
      long[] totalNanos = new long[n];
      Queue<TotalTimeVelIndex> pq = (equalTime && n != 0) ? new PriorityQueue<TotalTimeVelIndex>()
        : null;
      for (int i = 0; i < totalNanos.length; i++) {
        if (pq != null) {
          pq.add(new TotalTimeVelIndex(totalNanos[i], i));
        }
      }
      int velIndex = n == 0 ? -1 : (equalTime ? pq.remove().velIndex : 0);
      return new RefoldingTime(
        totalNanos, pq, velIndex, -1, (long) Math.ceil(updateVelocityS * 1e9));
    }

    public void updateTimeVelIndexAndStartTracking() {
      if (latestNanoTime != -1 & index != -1) {
        long t = System.nanoTime();
        Preconditions.checkState(latestNanoTime < t);
        if (t >= latestNanoTime + updateVelocityNs) {
          int newVelIndex = ApproachRetractionAssistant.updateTime(
            totalNanos, pq, index, latestNanoTime, t);
          latestNanoTime = t;
          index = newVelIndex;
        }
      }
    }

    public void cancelLatest() {
      latestNanoTime = -1;
    }
  }

  private enum PreserveRampSizeStrategy {
    NEVER, AFTER, ALWAYS;

    public static PreserveRampSizeStrategy parse(String s) {
      return PreserveRampSizeStrategy.valueOf(s.toUpperCase(Locale.ROOT));
    }
  }
}
